@bendyline/squisq-editor-react 2.11.6 → 2.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE.md CHANGED
@@ -9,22 +9,22 @@ Third-party components remain under their respective license terms.
9
9
  | Package | Version | License | Repository |
10
10
  | --------------------------------- | -------------------- | ----------------------------- | --------------------------------------------- |
11
11
  | @fortawesome/fontawesome-free | 7.2.0 | CC-BY-4.0 AND OFL-1.1 AND MIT | https://github.com/FortAwesome/Font-Awesome |
12
- | @tiptap/core | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
13
- | @tiptap/extension-heading | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
14
- | @tiptap/extension-image | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
15
- | @tiptap/extension-link | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
16
- | @tiptap/extension-mention | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
17
- | @tiptap/extension-placeholder | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
18
- | @tiptap/extension-table | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
19
- | @tiptap/extension-table-cell | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
20
- | @tiptap/extension-table-header | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
21
- | @tiptap/extension-table-row | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
22
- | @tiptap/extension-task-item | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
23
- | @tiptap/extension-task-list | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
24
- | @tiptap/pm | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
25
- | @tiptap/react | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
26
- | @tiptap/starter-kit | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
27
- | @tiptap/suggestion | 2.27.2 | MIT | https://github.com/ueberdosis/tiptap |
12
+ | @tiptap/core | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
13
+ | @tiptap/extension-heading | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
14
+ | @tiptap/extension-image | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
15
+ | @tiptap/extension-link | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
16
+ | @tiptap/extension-mention | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
17
+ | @tiptap/extension-placeholder | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
18
+ | @tiptap/extension-table | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
19
+ | @tiptap/extension-table-cell | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
20
+ | @tiptap/extension-table-header | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
21
+ | @tiptap/extension-table-row | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
22
+ | @tiptap/extension-task-item | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
23
+ | @tiptap/extension-task-list | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
24
+ | @tiptap/pm | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
25
+ | @tiptap/react | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
26
+ | @tiptap/starter-kit | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
27
+ | @tiptap/suggestion | 2.27.3 | MIT | https://github.com/ueberdosis/tiptap |
28
28
  | mermaid | 11.16.1 | MIT | https://github.com/mermaid-js/mermaid |
29
29
  | type-fest | 4.41.0 | MIT OR CC0-1.0 | https://github.com/sindresorhus/type-fest |
30
30
  | @monaco-editor/react _(bundled)_ | 4.7.0 | MIT | https://github.com/suren-atoyan/monaco-react |
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  markdownToTiptap
3
- } from "./chunk-ES6CPRCW.js";
3
+ } from "./chunk-MFE325P4.js";
4
4
 
5
5
  // src/blockRange.ts
6
6
  import { parseMarkdown } from "@bendyline/squisq/markdown";
@@ -273,6 +273,241 @@ import {
273
273
  DocumentVersionManager
274
274
  } from "@bendyline/squisq/versions";
275
275
 
276
+ // src/mediaEdit/mediaEditTargets.ts
277
+ import { collectDocMediaClips } from "@bendyline/squisq/mediaEdit";
278
+ import { splitKeyValueToken, tokenizeAttrTokens } from "@bendyline/squisq/markdown";
279
+
280
+ // src/embeddedMedia.ts
281
+ import { parseTimeSeconds } from "@bendyline/squisq/markdown";
282
+ import {
283
+ createMediaTimeMap,
284
+ mediaEditValuesFromAttributes,
285
+ parseMediaEdits
286
+ } from "@bendyline/squisq/mediaEdit";
287
+ import {
288
+ scheduleMediaClip
289
+ } from "@bendyline/squisq/schemas";
290
+ var VIDEO_EXT = /* @__PURE__ */ new Set(["webm", "mp4", "mov", "m4v", "ogv"]);
291
+ var AUDIO_EXT = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "oga", "m4a", "aac", "flac", "opus"]);
292
+ function mediaKindFromUrl(url) {
293
+ const ext = url.split("?")[0].split("#")[0].split(".").pop()?.toLowerCase();
294
+ if (!ext) return null;
295
+ if (VIDEO_EXT.has(ext)) return "video";
296
+ if (AUDIO_EXT.has(ext)) return "audio";
297
+ return null;
298
+ }
299
+ function mediaTimeAttribute(attributes, kind, name) {
300
+ const raw = attributes?.[`data-squisq-${kind}-${name}`];
301
+ if (raw == null) return void 0;
302
+ return parseTimeSeconds(raw) ?? void 0;
303
+ }
304
+ function collectEmbeddedMedia(block) {
305
+ const out = [];
306
+ const visit = (node, line) => {
307
+ if (!node || typeof node !== "object") return;
308
+ const n = node;
309
+ const here = n.position?.start?.line ?? line;
310
+ if ((n.type === "image" || n.type === "link") && typeof n.url === "string") {
311
+ const kind = mediaKindFromUrl(n.url);
312
+ if (kind) out.push({ src: n.url, kind, form: "link", sourceLine: here });
313
+ }
314
+ if (n.type === "htmlElement" && (n.tagName === "video" || n.tagName === "audio")) {
315
+ const kind = n.tagName === "video" ? "video" : "audio";
316
+ let src = typeof n.attributes?.src === "string" ? n.attributes.src : void 0;
317
+ if (!src && Array.isArray(n.children)) {
318
+ for (const child of n.children) {
319
+ const c = child;
320
+ if (c.tagName === "source" && typeof c.attributes?.src === "string") {
321
+ src = c.attributes.src;
322
+ break;
323
+ }
324
+ }
325
+ }
326
+ if (src) {
327
+ const startAt = mediaTimeAttribute(n.attributes, kind, "start-at");
328
+ const clipStart = mediaTimeAttribute(n.attributes, kind, "clip-start");
329
+ const clipEnd = mediaTimeAttribute(n.attributes, kind, "clip-end");
330
+ const edits = parseMediaEdits(mediaEditValuesFromAttributes(kind, n.attributes ?? {}));
331
+ out.push({
332
+ src,
333
+ kind,
334
+ form: "html",
335
+ sourceLine: here,
336
+ ...startAt != null ? { startAt } : {},
337
+ ...clipStart != null ? { clipStart } : {},
338
+ ...clipEnd != null ? { clipEnd } : {},
339
+ ...edits ? { edits } : {}
340
+ });
341
+ }
342
+ }
343
+ if (Array.isArray(n.children)) n.children.forEach((c) => visit(c, here));
344
+ if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach((c) => visit(c, here));
345
+ };
346
+ (block.contents ?? []).forEach((node) => visit(node, void 0));
347
+ return out;
348
+ }
349
+ function resolveEmbeddedMediaTiming(block, media) {
350
+ const startAt = Math.max(0, media.startAt ?? 0);
351
+ const sourceIn = Math.max(0, media.clipStart ?? 0);
352
+ const absoluteStart = block.startTime + startAt;
353
+ const blockEnd = block.startTime + block.duration;
354
+ const authoredLength = media.clipEnd == null ? null : createMediaTimeMap({ clipStart: sourceIn, clipEnd: media.clipEnd, cuts: media.edits?.cuts }).playedDuration;
355
+ const absoluteEnd = authoredLength == null ? blockEnd : Math.min(blockEnd, absoluteStart + authoredLength);
356
+ return {
357
+ absoluteStart,
358
+ absoluteEnd: Math.max(absoluteStart, absoluteEnd),
359
+ sourceIn
360
+ };
361
+ }
362
+ function collectEmbeddedMediaSchedule(doc, opts) {
363
+ const schedule = [];
364
+ const visit = (blocks) => {
365
+ for (const block of blocks) {
366
+ collectEmbeddedMedia(block).forEach((media, index) => {
367
+ const timing = resolveEmbeddedMediaTiming(block, media);
368
+ const clip = {
369
+ id: `embedded:${block.id}:${index}`,
370
+ src: media.src,
371
+ kind: media.kind,
372
+ startAt: media.startAt ?? 0,
373
+ anchor: "block",
374
+ ...media.clipStart != null ? { clipStart: timing.sourceIn } : {},
375
+ ...media.clipEnd != null ? { clipEnd: media.clipEnd } : {},
376
+ ...media.edits ? { edits: media.edits } : {},
377
+ ...media.sourceLine != null ? { sourceLine: media.sourceLine } : {}
378
+ };
379
+ schedule.push(
380
+ ...scheduleMediaClip(
381
+ clip,
382
+ timing.absoluteStart,
383
+ timing.absoluteEnd,
384
+ "block",
385
+ block.id,
386
+ opts
387
+ )
388
+ );
389
+ });
390
+ if (block.children?.length) visit(block.children);
391
+ }
392
+ };
393
+ visit(doc.blocks);
394
+ return schedule;
395
+ }
396
+ function collectEmbeddedVideoSchedule(doc) {
397
+ return collectEmbeddedMediaSchedule(doc).filter((clip) => clip.kind === "video");
398
+ }
399
+ function collectTimelinePlaybackSchedule(doc, scheduled, opts) {
400
+ const scheduledSources = new Set(scheduled.map((clip) => clip.src));
401
+ const narration = doc.audio.segments.map((segment, index) => {
402
+ if (scheduledSources.has(segment.src)) return null;
403
+ return {
404
+ id: `narration:${index}`,
405
+ kind: "audio",
406
+ src: segment.src,
407
+ absoluteStart: Math.max(0, segment.startTime),
408
+ absoluteEnd: Math.max(0, segment.startTime) + Math.max(0, segment.duration),
409
+ sourceIn: 0,
410
+ anchor: "document"
411
+ };
412
+ }).filter(
413
+ (clip) => clip !== null && clip.absoluteEnd > clip.absoluteStart
414
+ );
415
+ return [...scheduled, ...collectEmbeddedMediaSchedule(doc, opts), ...narration];
416
+ }
417
+
418
+ // src/mediaEdit/mediaEditTargets.ts
419
+ function flatten(blocks, out = []) {
420
+ for (const block of blocks) {
421
+ out.push(block);
422
+ if (block.children?.length) flatten(block.children, out);
423
+ }
424
+ return out;
425
+ }
426
+ function listEditableMedia(doc) {
427
+ const items = [];
428
+ for (const clip of collectDocMediaClips(doc)) {
429
+ if (clip.sourceLine == null) continue;
430
+ items.push({
431
+ src: clip.src,
432
+ kind: clip.kind,
433
+ sourceLine: clip.sourceLine,
434
+ ...clip.edits ? { edits: clip.edits } : {},
435
+ ...clip.clipStart != null ? { clipStart: clip.clipStart } : {},
436
+ ...clip.clipEnd != null ? { clipEnd: clip.clipEnd } : {},
437
+ startAt: clip.startAt,
438
+ clip
439
+ });
440
+ }
441
+ for (const block of flatten(doc.blocks)) {
442
+ collectEmbeddedMedia(block).forEach((media, index) => {
443
+ if (media.sourceLine == null || media.form !== "html") return;
444
+ const clip = {
445
+ id: `embedded:${block.id}:${index}`,
446
+ src: media.src,
447
+ kind: media.kind,
448
+ startAt: media.startAt ?? 0,
449
+ anchor: "block",
450
+ sourceLine: media.sourceLine,
451
+ ...media.clipStart != null ? { clipStart: media.clipStart } : {},
452
+ ...media.clipEnd != null ? { clipEnd: media.clipEnd } : {},
453
+ ...media.edits ? { edits: media.edits } : {}
454
+ };
455
+ items.push({
456
+ src: media.src,
457
+ kind: media.kind,
458
+ sourceLine: media.sourceLine,
459
+ ...media.edits ? { edits: media.edits } : {},
460
+ ...media.clipStart != null ? { clipStart: media.clipStart } : {},
461
+ ...media.clipEnd != null ? { clipEnd: media.clipEnd } : {},
462
+ ...media.startAt != null ? { startAt: media.startAt } : {},
463
+ clip
464
+ });
465
+ });
466
+ }
467
+ const seen = /* @__PURE__ */ new Set();
468
+ return items.sort((a, b) => a.sourceLine - b.sourceLine).filter((item) => seen.has(item.sourceLine) ? false : (seen.add(item.sourceLine), true));
469
+ }
470
+ function findEditableMedia(doc, target) {
471
+ const items = listEditableMedia(doc);
472
+ if (target.sourceLine != null) {
473
+ const byLine = items.find((item) => item.sourceLine === target.sourceLine);
474
+ if (byLine) return byLine;
475
+ }
476
+ return items.find((item) => item.src === target.src) ?? null;
477
+ }
478
+ function groupCompanions(doc, item) {
479
+ const group = item.edits?.group;
480
+ if (!group) return [];
481
+ return listEditableMedia(doc).filter(
482
+ (other) => other.sourceLine !== item.sourceLine && other.edits?.group === group
483
+ );
484
+ }
485
+ function mediaTargetFromAnnotationText(text) {
486
+ const match = text.trim().match(/^\{\[(audio|video|media)\b([\s\S]*)\]\}$/);
487
+ if (!match) return null;
488
+ let src;
489
+ for (const token of tokenizeAttrTokens(match[2].trim())) {
490
+ const kv = splitKeyValueToken(token);
491
+ if (kv?.key === "src") src = kv.value;
492
+ }
493
+ if (!src) return null;
494
+ return { src, kind: match[1] === "video" ? "video" : "audio" };
495
+ }
496
+ function mediaTargetFromElement(element) {
497
+ const node = element.closest("[data-squisq-media-src]");
498
+ if (node) {
499
+ const src = node.dataset.squisqMediaSrc;
500
+ const kind = node.dataset.squisqMediaKind === "video" ? "video" : "audio";
501
+ return src ? { src, kind } : null;
502
+ }
503
+ const paragraph = element.closest("p");
504
+ return paragraph ? mediaTargetFromAnnotationText(paragraph.textContent ?? "") : null;
505
+ }
506
+ function embeddedMediaClips(doc) {
507
+ const scheduled = new Set(collectDocMediaClips(doc).map((clip) => clip.sourceLine));
508
+ return listEditableMedia(doc).filter((item) => item.edits && !scheduled.has(item.sourceLine)).map((item) => item.clip);
509
+ }
510
+
276
511
  // src/scene/text/sceneTextChannel.ts
277
512
  function createSceneTextChannel() {
278
513
  let current = null;
@@ -342,6 +577,10 @@ function useEditorContext() {
342
577
  }
343
578
  return ctx;
344
579
  }
580
+ function useEditorContextOptional() {
581
+ return useContext(EditorContext);
582
+ }
583
+ var MEDIA_EDIT_GC_DELAY_MS = 5e3;
345
584
  var DEFAULT_PRUNE_POLICY = { type: "keep-last-n", n: 50 };
346
585
  var DEFAULT_AUTOSAVE_IDLE_MS = 5e3;
347
586
  function normalizeBlockTagVisibility(value) {
@@ -361,6 +600,7 @@ function EditorProvider({
361
600
  versioningAutoSaveIdleMs = DEFAULT_AUTOSAVE_IDLE_MS,
362
601
  onSaveVersion,
363
602
  mediaProvider = null,
603
+ mediaEditRenders = null,
364
604
  imageDisplayMode = "inline",
365
605
  mentionProvider = null,
366
606
  proofing = null,
@@ -488,6 +728,13 @@ function EditorProvider({
488
728
  const closeImageEdit = useCallback2(() => {
489
729
  setImageEditTarget(null);
490
730
  }, []);
731
+ const [mediaEditTarget, setMediaEditTarget] = useState2(null);
732
+ const openMediaEdit = useCallback2((target) => {
733
+ setMediaEditTarget(target);
734
+ }, []);
735
+ const closeMediaEdit = useCallback2(() => {
736
+ setMediaEditTarget(null);
737
+ }, []);
491
738
  const bumpMediaRevision = useCallback2(() => {
492
739
  setMediaRevision((n) => n + 1);
493
740
  }, []);
@@ -886,6 +1133,20 @@ function EditorProvider({
886
1133
  setParseError(err instanceof Error ? err.message : "Stringify error");
887
1134
  }
888
1135
  }, []);
1136
+ useEffect(() => {
1137
+ if (mediaEditRenders && doc) mediaEditRenders.sync(doc, embeddedMediaClips(doc));
1138
+ }, [mediaEditRenders, doc]);
1139
+ const latestDocRef = useRef(doc);
1140
+ latestDocRef.current = doc;
1141
+ useEffect(() => {
1142
+ if (!mediaEditRenders) return;
1143
+ const timer = setTimeout(() => {
1144
+ const current = latestDocRef.current;
1145
+ if (current) void mediaEditRenders.collectGarbage(current).catch(() => {
1146
+ });
1147
+ }, MEDIA_EDIT_GC_DELAY_MS);
1148
+ return () => clearTimeout(timer);
1149
+ }, [mediaEditRenders]);
889
1150
  const value = useMemo2(
890
1151
  () => ({
891
1152
  markdownSource,
@@ -911,6 +1172,7 @@ function EditorProvider({
911
1172
  activeBlockStartLine,
912
1173
  selectionVersion,
913
1174
  imageEditTarget,
1175
+ mediaEditTarget,
914
1176
  mediaRevision,
915
1177
  allowRecording,
916
1178
  allowNarrate,
@@ -923,6 +1185,7 @@ function EditorProvider({
923
1185
  versioning,
924
1186
  saveVersion,
925
1187
  mediaProvider,
1188
+ mediaEditRenders,
926
1189
  imageDisplayMode,
927
1190
  mentionProvider,
928
1191
  proofing,
@@ -965,6 +1228,8 @@ function EditorProvider({
965
1228
  replaceAll,
966
1229
  openImageEdit,
967
1230
  closeImageEdit,
1231
+ openMediaEdit,
1232
+ closeMediaEdit,
968
1233
  bumpMediaRevision
969
1234
  }),
970
1235
  [
@@ -998,6 +1263,7 @@ function EditorProvider({
998
1263
  versioning,
999
1264
  saveVersion,
1000
1265
  mediaProvider,
1266
+ mediaEditRenders,
1001
1267
  imageDisplayMode,
1002
1268
  mentionProvider,
1003
1269
  proofing,
@@ -1039,12 +1305,15 @@ function EditorProvider({
1039
1305
  insertAtCursor,
1040
1306
  replaceAll,
1041
1307
  imageEditTarget,
1308
+ mediaEditTarget,
1042
1309
  mediaRevision,
1043
1310
  allowRecording,
1044
1311
  allowNarrate,
1045
1312
  preserveSourceWrapping,
1046
1313
  openImageEdit,
1047
1314
  closeImageEdit,
1315
+ openMediaEdit,
1316
+ closeMediaEdit,
1048
1317
  bumpMediaRevision
1049
1318
  ]
1050
1319
  );
@@ -1628,7 +1897,15 @@ export {
1628
1897
  detectLanguageFromFileName,
1629
1898
  resolveFileKind,
1630
1899
  useBlockNavigator,
1900
+ collectEmbeddedMedia,
1901
+ resolveEmbeddedMediaTiming,
1902
+ collectEmbeddedVideoSchedule,
1903
+ collectTimelinePlaybackSchedule,
1904
+ findEditableMedia,
1905
+ groupCompanions,
1906
+ mediaTargetFromElement,
1631
1907
  useEditorContext,
1908
+ useEditorContextOptional,
1632
1909
  EditorProvider,
1633
1910
  reviewUnderlineClass,
1634
1911
  findUniqueTextRange,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  markdownToTiptap,
3
3
  tiptapToMarkdown
4
- } from "./chunk-ES6CPRCW.js";
4
+ } from "./chunk-MFE325P4.js";
5
5
 
6
6
  // src/jsonEditor/JsonEditorContext.tsx
7
7
  import { createContext, useContext, useEffect, useMemo } from "react";